package service

import (
	"GopherFocus/app/dao"
	"GopherFocus/app/model"
	"GopherFocus/app/shared"
	"GopherFocus/app/system/index/internal/define"
	"context"
	"github.com/gogf/gf/database/gdb"
	"github.com/gogf/gf/encoding/ghtml"
	"github.com/gogf/gf/frame/g"
	"github.com/gogf/gf/util/gutil"
)

var Content = contentService{}

type contentService struct{}

// 结果引导过程法则：即看结果需要什么，就写程序获取什么，例如输出的结构体中包含哪些字段，这些字段哪些是可以从输入字段中获取或加工出来的
// 查询内容列表
func (s *contentService) GetList(ctx context.Context, input define.ContentGetListInput) (output *define.ContentGetListOutput, err error) {
	var (
		// dao：data access object(数据访问对象)
		m = dao.Content.Ctx(ctx)
	)
	output = &define.ContentGetListOutput{
		Page: input.Page,
		Size: input.Size,
	}
	// 默认查询topic
	if input.Type == "" {
		// 链式操作
		m = m.Where(dao.Content.C.Type, input.Type)
	} else {
		// 默认类型是topic类型
		m = m.Where(dao.Content.C.Type, model.ContentTypeTopic)
	}
	// 栏目检索
	if input.CategoryId > 0 {
		idArray, err := Category.GetSubIdList(ctx, input.CategoryId)
		if err != nil {
			return output, err
		}
		// 获取该栏目下的所有内容
		m = m.Where(dao.Content.C.CategoryId, idArray)
	}
	// 如果不是管理员，则只能看到自己创建的Content
	// 管理员可以查看所有内容
	if input.UserId > 0 && !User.IsAdminShow(ctx, input.UserId) {
		m = m.Where(dao.Content.C.UserId, input.UserId)
	}
	// 分页查询, 两个参数：Page, limit; Page表示页码，limit表示每页数量
	listModel := m.Page(input.Page, input.Size)
	// 排序
	switch input.Sort {
	case model.ContentSortHot:
		listModel.OrderDesc(dao.Content.C.ViewCount)
	case model.ContentSortActive:
		listModel.OrderDesc(dao.Content.C.UpdatedAt)
	default:
		listModel.OrderDesc(dao.Content.C.Id)
	}
	// 执行查询
	var list []*model.Content
	if err := listModel.Scan(&list); err != nil {
		return output, err
	}
	// 没有数据
	if len(list) == 0 {
		return output, nil
	}
	// 查询总数量
	output.Total, err = m.Count()
	if err != nil {
		return output, err
	}
	// 获取Content字段内容
	// ScanList表示将查询到的数据数组绑定到 output.List列表中的每一项Content属性上
	// output.List的Content属性对应的结构体类型 和 查询结果的结构体类型 可以不一致???
	if err := listModel.ScanList(&output.List, "Content"); err != nil {
		return output, err
	}
	// 获取Category
	// Fields用于选择字段，可以传string/Map/struct
	//
	// 假设list的结构为：
	//	type KeyEntity struct {
	//		SubKey string
	//	}
	//
	//	type Item struct {
	//		Key KeyEntity
	//	}
	// var list []Item
	// gutil.ListItemValues(list, "Key", "SubKey") 是从list切片中选择每一个Item的Key属性的SubKey属性，然后构成一个[]interface{}返回
	// gutil.ListItemValuesUnique返回的[]interface{}是去重后的结果
	//
	// select Id, Name, Thumb, ContentType from Category where id in (CategoryId1, CategoryId2......);
	// ScanList表示将上一步查询的结果切片，绑定到output.List中的每一项的Category属性，
	// 然后会根据Category表的id字段与Content结构体的CategoryId属性进行绑定，
	// 例如 上一步查询出来的Category的Id对应列表为[1, 2, 3, 4], output.List中的Content对应的CategorId为[2, 4, 1, 3]
	// 则Id为1的Category对象会绑定到output.List[2]中的Category属性
	err = dao.Category.
		Fields(model.ContentListCategoryItem{}).
		Where(dao.Category.C.Id, gutil.ListItemValuesUnique(output.List, "Content", "CategoryId")).
		ScanList(&output.List, "Category", "Content", "id:CategoryId")
	if err != nil {
		return output, err
	}
	// 获取User
	err = dao.User.
		Fields(model.ContentListUserItem{}).
		Where(dao.User.C.Id, gutil.ListItemValuesUnique(output.List, "Content", "UserId")).
		ScanList(&output.List, "User", "Content", "id:UserId")
	if err != nil {
		return output, err
	}
	return output, nil
}

// 查询详情信息(详情中包含User和Content)
func (s *contentService) GetDetail(ctx context.Context, id uint) (output *define.ContentGetDetailOutput, err error) {
	output = &define.ContentGetDetailOutput{}
	// WherePri(xx) 将传入的值作为主键来查询： where id=xx;
	if err := dao.Content.Ctx(ctx).WherePri(id).Scan(&output.Content); err != nil {
		return nil, err
	}
	// 如果没有，则查出来为nil
	if output.Content == nil {
		return nil, nil
	}
	err = dao.User.Ctx(ctx).WherePri(output.Content.UserId).Scan(&output.User)
	if err != nil {
		return nil, nil
	}
	return output, nil
}

// 添加浏览量
func (s *contentService) AddViewCount(ctx context.Context, id uint, count int) error {
	return dao.Content.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
		_, err := dao.Content.Ctx(ctx).WherePri(id).Increment(dao.Content.C.ViewCount, count)
		if err != nil {
			return err
		}
		return nil
	})
}

// 增加Content回复次数
func (s *contentService) AddReplyCount(ctx context.Context, id uint, count int) error {
	return dao.Reply.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
		_, err := dao.Content.Ctx(ctx).WherePri(id).Increment(dao.Content.C.ReplyCount, count)
		if err != nil {
			return err
		}
		return nil
	})
}

// 采纳回复
// 即更新Content的adopted_reply_id
func (s *contentService) AdoptReply(ctx context.Context, id, replyId uint) error {
	return dao.Content.Ctx(ctx).Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
		// 这里之所以使用UserId和Id进行数据过滤，是为了使当前用户只能修改自己发布的Content
		// dao.Content.C.UserId == User.Id，可以确保是当前用户自己发布的Content，
		// WherePri(id)根据主键过滤出需要修改的Content
		//
		// Data(dao.Content.C.AdoptedReplyId, replyId) 表示 仅对dao.Content.C.AdoptedReplyId这一列进行操作
		_, err := dao.Content.Ctx(ctx).
			Data(dao.Content.C.AdoptedReplyId, replyId).
			Where(dao.Content.C.UserId, shared.Context.Get(ctx).User.Id).
			WherePri(id).
			Update()
		return err
	})
}

// 取消采纳回复
func (s *contentService) UnacceptedReply(ctx context.Context, id uint) error {
	return dao.Content.Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
		_, err := dao.Content.Ctx(ctx).
			Data(dao.Content.C.AdoptedReplyId, 0).
			WherePri(id).
			Update()
		if err != nil {
			return err
		}
		return nil
	})
}

// 搜索内容列表
// 先根据input传入的参数查询Content，然后分别根据Content的CategoryId与UserId关联Category和User
// dao.Content.Ctx(ctx)
// WhereLike(a, "%b%") => where a like %b%;
// WhereOrLike => or xx like xx
// 	all, err := listModel.All()
//	if err != nil {
//		return nil, err
//	}
//	// 如果没有相应的搜索结果，直接返回
//	if all.IsEmpty() {
//		return nil, nil
//	}
// 	err = dao.Category.
//		Fields(model.ContentListCategoryItem{}).
//		Where(dao.Category.C.Id, gutil.ListItemValuesUnique(output.List, "Content", "CategoryId")).
//		ScanList(&output.List, "Category", "Content", "id:CategoryId")
//	if err != nil {
//		return nil, err
//	}
//
// 	statsModel := m.Fields(dao.Content.C.Type, "count(*) as total").Group(dao.Content.C.Type)
//	statsAll, err := statsModel.All()
//	if err != nil {
//		return nil, err
//	}
//	output.Stats = make(map[string]int)
//	for _, v := range statsAll {
//		output.Stats[v["type"].String()] = v["total"].Int()
//	}
func (s *contentService) Search(ctx context.Context, input define.ContentSearchInput) (output *define.ContentSearchOutput, err error) {
	// 1.根据input传入参数查询Content
	var (
		m = dao.Content.Ctx(ctx)
		// 查询关键字
		likePattern = `%` + input.Key + `%`
	)
	// 为output分配内容，只所有返回值为指针，是不是为了节省内存？？？
	output = &define.ContentSearchOutput{
		Page: input.Page,
		Size: input.Size,
	}
	// 根据其他input参数构造查询条件
	// 根据Content内容和标题进行模糊查询
	m = m.WhereLike(dao.Content.C.Content, likePattern).WhereOrLike(dao.Content.C.Title, likePattern)
	if input.Type != "" {
		m = m.Where(dao.Content.C.Type, input.Type)
	}
	if input.CategoryId > 0 {
		idArray, err := Category.GetSubIdList(ctx, input.CategoryId)
		if err != nil {
			return nil, err
		}
		m = m.Where(dao.Content.C.CategoryId, idArray)
	}
	// 分页和排序
	listModel := m.Page(input.Page, input.Size)
	switch input.Sort {
	// 按照热度降序排序，浏览量越多热度越高
	case model.ContentSortHot:
		listModel = listModel.OrderDesc(dao.Content.C.ViewCount)
	// 按照更新时间排序，时间越新越Active
	case model.ContentSortActive:
		listModel = listModel.OrderDesc(dao.Content.C.UpdatedAt)
	default:
		// 默认按照ID排序
		listModel = listModel.OrderDesc(dao.Content.C.Id)
	}
	// // 如果没有相应的搜索结果，直接返回
	all, err := listModel.All()
	if err != nil {
		return nil, err
	}
	if all.IsEmpty() {
		return nil, nil
	}
	// 查询搜索结果总数
	output.Total, err = m.Count()
	if err != nil {
		return nil, err
	}
	// 计算output.Stats(搜索结果统计)，统计搜索结果中每种类型的Content数量
	// 需要对搜索结果类型进行分组
	statsModel := m.Fields(dao.Content.C.Type, "count(*) as total").Group(dao.Content.C.Type)
	statsAll, err := statsModel.All()
	if err != nil {
		return nil, err
	}
	output.Stats = make(map[string]int)
	for _, v := range statsAll {
		output.Stats[v["type"].String()] = v["total"].Int()
	}

	// 2.根据查询获取的Content关联Category
	err = dao.Category.
		Fields(model.ContentListCategoryItem{}).
		Where(dao.Category.C.Id, gutil.ListItemValuesUnique(output.List, "Content", "CategoryId")).
		ScanList(&output.List, "Category", "Content", "id:CategoryId")
	if err != nil {
		return nil, err
	}

	// 3.根据查询获取的Content关联User
	err = dao.User.
		Fields(model.ContentListUserItem{}).
		Where(dao.Content.C.Id, gutil.ListItemValuesUnique(output.List, "Content", "UserId")).
		ScanList(&output.List, "User", "Content", "id:UserId")
	if err != nil {
		return nil, err
	}
	return output, nil
}

// 创建内容
func (s *contentService) Create(ctx context.Context, input define.ContentCreateInput) (output define.ContentCreateOutput, err error) {
	// ??????
	if input.UserId == 0 {
		input.UserId = shared.Context.Get(ctx).User.Id
	}
	// 不允许HTML代码
	if err = ghtml.SpecialCharsMapOrStruct(input); err != nil {
		return output, err
	}
	// 这里不需要加OmitEmpty，因为如果创建时某些内容为空也是可以的
	lastInsertId, err := dao.Content.Ctx(ctx).Data(input).InsertAndGetId()
	if err != nil {
		return output, err
	}
	return define.ContentCreateOutput{
		ContentId: uint(lastInsertId),
	}, nil
}

// FieldsEx(dao.Content.C.Id)表示不包含Id字段（即排除的意思,exclude）
// 更新和删除都用事务，插入不使用事务，为什么？？？？
func (s *contentService) Update(ctx context.Context, input define.ContentUpdateInput) (err error) {
	return dao.Content.Ctx(ctx).Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
		// 不允许HTML代码
		if err := ghtml.SpecialCharsMapOrStruct(input); err != nil {
			return err
		}

		// FieldsEx(dao.Content.C.Id)表示不包含Id字段（即排除的意思,exclude）
		_, err := dao.Content.Ctx(ctx).Data(input).
			FieldsEx(dao.Content.C.Id).
			Where(dao.Content.C.Id, input.Id).
			Where(dao.Content.C.UserId, shared.Context.Get(ctx).User.Id).
			Update()
		return err
	})
}

// 删除内容
// _, err := xxx
// if err == nil {xx}
// return err
func (s *contentService) Delete(ctx context.Context, id uint) (err error) {
	return dao.Content.Ctx(ctx).Transaction(ctx, func(ctx context.Context, tx *gdb.TX) error {
		user := shared.Context.Get(ctx).User
		// 管理员直接删除内容和评论
		// 管理员删除的是指定Content及其所有评论
		if user.IsAdmin {
			_, err := dao.Content.Ctx(ctx).Where(dao.Content.C.Id, id).Delete()
			if err == nil {
				_, err = dao.Reply.Ctx(ctx).Where(dao.Reply.C.TargetId, id).Delete()
			}
			return err
		}
		// 之所以同时用dao.Content.C.Id和dao.Content.C.UserId对数据进行过滤
		// 是为了确保当前用户只能修改自己发布的内容
		_, err := dao.Content.Ctx(ctx).Where(g.Map{
			dao.Content.C.Id:     id,
			dao.Content.C.UserId: user.Id,
		}).Delete()
		// 删除评论
		// 普通成员删除的是该用户自己的对该Content的评论
		if err == nil {
			err = Reply.DeleteByUserContentId(ctx, user.Id, id)
		}
		return err
	})
}
